/************************************************************************************
 *                                .                                                 *
 *                                .                                                 *
 ************************************************************************************/
package jMiiEditor.mii;

import jMiiEditor.mii.introspection.MiiGetterIntrospection;
import jMiiEditor.mii.introspection.MiiGetterIntrospectionBadParameterException;
import jMiiEditor.mii.introspection.MiiGetterIntrospectionItem;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

import junit.framework.Assert;
import junit.framework.TestCase;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import cx.ath.choisnet.lang.introspection.IntrospectionException;
import cx.ath.choisnet.lang.introspection.IntrospectionInvokeException;
import cx.ath.choisnet.lang.introspection.method.IntrospectionCompareException;
import utils.BinStuffs;

/**
 * @author CC
 *
 */
public class MiiTest extends TestCase
{
    private static Logger slogger = Logger.getLogger(MiiTest.class);
    private static final boolean MAX_BOOL = true;
    private static final boolean MIN_BOOL = false;
    private static final byte[]  MAX_BYTES4 = { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF };
    private static final byte[]  MIN_BYTES4 = { (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00 };
    private static final int     MAX_INT = 0x00FF;
    private static final int     MIN_INT = 0;
    private static final String  MAX_RAWSTRING = "\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF\uFFFF";
    private static final String  MIN_RAWSTRING = "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000";
    //private static final File TEMP_DIR_FILE = new File( System.getProperty("java.io.tmpdir" ) );

    private final Mii minMii = MiiTestHelper.getMinMii();
    //private final Mii maxMii = MiiTestHelper.getMaxMii();
    private Mii refMii;
    private Mii avatarFileMii;
    private MiiGetterIntrospection miiGetterIntrospection;
    private File file;

    /** Performs initializations */
    public void setUp() throws IOException
    {
        slogger.setLevel( Level.ALL );

        this.miiGetterIntrospection = new MiiGetterIntrospection();
        slogger.info( "---------- Load a Mii -----------------------------------");
        InputStream is = getClass().getResourceAsStream("/jMiiEditor/mii/Mii.mii");
        refMii = new Mii( is );
        is.close();

        this.file = File.createTempFile( "avatar", ".mii" );
        refMii.save( file );

        slogger.info( "--- Load Mii : " + file.getCanonicalPath() );
        this.avatarFileMii = new Mii( file );
    }

    /**
     * use introspection to check hex values
     * @throws IllegalArgumentException  should not occur
     * @throws IllegalAccessException should not occur
     */
    public void testMASK() throws IllegalArgumentException, IllegalAccessException
    {
        slogger.info( this.getClass().getName() + ".testMASK()" );

        Field[] fields = Mii.class.getFields();

        slogger.info( "Found " + fields.length + " fields." );

        for( Field f : fields ) {
            if( f.getName().startsWith( "MASK_" )) {
                String binMaskStr = f.getName().substring(5);
                int    binMaskInt = 0x00FF & Integer.parseInt( binMaskStr, 2 );
                int    value      = 0x00FF & f.getByte( minMii );

                slogger.info( "Inspect field: " + f.getName() + " [%" + binMaskStr + "/" + binMaskInt + "]=[" + value + "]?" );

                Assert.assertEquals( binMaskInt, value );
            }
            else {
                slogger.info( "Ignored field: " + f.getName() );
            }
        }
    }

    public void testThisCONSTANTS()
    {
        assertTrue( "MAX_RAWSTRING.length() = " + MAX_RAWSTRING.length(), MAX_RAWSTRING.length() == 10);
        assertTrue( "MIN_RAWSTRING.length() = " + MIN_RAWSTRING.length(), MIN_RAWSTRING.length() == 10);

//        byte[] maxMiiBytes = this.maxMii.get
    }

    private final static boolean isIgnored( Method m )
    {
        String name = m.getName();

        if( name.equals( "getClass" ) ) {
            return true;
        }
        else if( name.equals( "getBytesCopy" ) ) {
            return true;
        }

        return false;
    }

    /**
     * Verify that getter and setter are consistent according to type and annotations
     * @throws SecurityException
     */
    public void testGetterSetterMatch() throws SecurityException
    {
        Method[] methods = Mii.class.getMethods(); // Only public !

        for( Method m : methods ) {
            if( isIgnored( m ) ) {
                continue;
            }

            String name = m.getName();
            NoSuchMethodException nsme = null;
            Method getter;
            Method setter;

            if( name.startsWith( "is" ) ) {
                // Seem to be a getter !
                String     beanName    = name.substring( 2 );
                Class<?>[] paramsTypes = m.getParameterTypes();
                Class<?>   returnType  = m.getReturnType();

                if(paramsTypes.length == 0 ) {
                    getter = m;

                    // try to find setter !
                    try {
                        setter = Mii.class.getMethod( "set" + beanName, returnType );
                    }
                    catch( NoSuchMethodException e ) {
                        setter = null;
                        nsme = e;
                    }
                }
                else {
                    // Not a getter
                    getter = setter = null;
                }
            }
            else if( name.startsWith( "get") ) {
                // Seem to be a getter !
                String     beanName    = name.substring( 3 );
                Class<?>[] paramsTypes = m.getParameterTypes();
                Class<?>   returnType  = m.getReturnType();

                if(paramsTypes.length == 0 ) {
                    getter = m;

                    Class<?>[] paramsType = new Class<?>[1];
                    paramsType[0] = returnType;

                    // try to find setter !
                    try {
                        setter = Mii.class.getMethod( "set" + beanName, paramsType );
                    }
                    catch( NoSuchMethodException e ) {
                        setter = null;
                        nsme = e;
                    }
                }
                else {
                    // Not a getter
                    getter = setter = null;
                }
            }
            else if( name.startsWith( "set" ) ) {
                // Seem to be a setter !
                String     beanName    = name.substring( 3 );
                Class<?>[] paramsTypes = m.getParameterTypes();

                if(paramsTypes.length == 1 ) {
                    setter = m;
                    Class<?>[] noParams = new Class<?>[0];
                    // try to find setter !
                    try {
                        getter = Mii.class.getMethod( "get" + beanName, noParams );
                    }
                    catch( NoSuchMethodException e ) {
                        nsme = e;
                        try {
                            getter = Mii.class.getMethod( "is" + beanName, noParams );
                        }
                        catch( NoSuchMethodException e1 ) {
                            getter = null;
                        }
                    }
                }
                else {
                    // Not a getter
                    getter = setter = null;
                }
            }
            else {
                getter = setter = null;
            }

            if( getter == null ) {
                if( setter == null ) {
                    // Ok, not getter/setter
                }
                else {
                    slogger.fatal( "need a getter", nsme );
                    fail( "Found a setter " + setter + " but no getter !" );
                }
            }
            else {
                if( setter == null ) {
                    slogger.fatal( "need a setter", nsme );
                    fail( "Found a getter " + getter.getName() + " but no setter !" );
                }
                else {
                    // Check annotations !
                    MIIGETTER ganno = getter.getAnnotation( MIIGETTER.class );
                    assertNotNull( "No annotation for " + getter.getName(), ganno );
                    MIIGETTER sanno = setter.getAnnotation( MIIGETTER.class );
                    assertNotNull( "No annotation for " + setter.getName(), sanno );

                    assertEquals("Annotation mismatch for: " + getter.getName() + " / " + setter.getName() + " - annotations getter:" + ganno + ", setter:" + sanno,ganno,sanno);

                    // TODO: trying to check mask if integer !
                    // Perhaps not here ?
                }
            }
        }
    }

    /**
     * Set values on all setter, and verify value using getter Use a
     * "empty" Mii and set max values with setter (one to one) and verify that
     * only matching getter has change.
     * @throws IntrospectionInvokeException
     * @throws MiiGetterIntrospectionBadParameterException
     */
    public void testGetterSetterEffects() throws IntrospectionInvokeException, MiiGetterIntrospectionBadParameterException
    {
        slogger.info( "---------- testGetterSetterEffects()");
        Map<String,MiiGetterIntrospectionItem> items = miiGetterIntrospection.getMap();
        final byte[] emptyBytes = new byte[ minMii.length() ];
        byte[]       xorBytes   = new byte[ minMii.length() ];

        for( MiiGetterIntrospectionItem item : items.values() ) {
            Mii cpy = new Mii( minMii );

            //MIIGETTER      anno = item.getAnnotation();
            MIIGETTER.Type type = item.getType();

            // set MAX value
            if( type == MIIGETTER.Type.BOOL ) {
                item.setBooleanValue( cpy, MAX_BOOL );
            } else if( type == MIIGETTER.Type.BYTES4 ) {
                item.setObjectValue( cpy, MAX_BYTES4 );
            } else if( type == MIIGETTER.Type.INT ) {
                item.setIntegerValue( cpy, MAX_INT );
            } else if( type == MIIGETTER.Type.RAWSTRING ) {
                item.setStringValue( cpy, MAX_RAWSTRING );
            } else {
                fail( "Unkwon type  MIIGETTER.Type = " + type );
            }

            byte[] bytesDiff = cpy.xorOperator( xorBytes );

            // some change ?
            assertFalse( "Not change found", Arrays.equals( emptyBytes, bytesDiff ) );

            byte[] alreadySet = cpy.andOperator( xorBytes );

            // overlap ?
            assertTrue( "Overlap when apply: " + item.getSetterMethod(), Arrays.equals( emptyBytes, alreadySet ) );

            // store modifications for future use
            xorBytes = cpy.orOperator( xorBytes );

            // orOperator() work ?
            assertFalse( "orOperator() change nothing : " + item.getSetterMethod(), Arrays.equals( emptyBytes, xorBytes ) );

            // set MIN value
            if( type == MIIGETTER.Type.BOOL ) {
                item.setBooleanValue( cpy, MIN_BOOL );
            } else if( type == MIIGETTER.Type.BYTES4 ) {
                item.setObjectValue( cpy, MIN_BYTES4 );
            } else if( type == MIIGETTER.Type.INT ) {
                item.setIntegerValue( cpy, MIN_INT );
            } else if( type == MIIGETTER.Type.RAWSTRING ) {
                item.setStringValue( cpy, MIN_RAWSTRING );
            } else {
                fail( "Unkwon type  MIIGETTER.Type = " + type );
            }

            String cmpStr = minMii.advanceCompareTo( cpy );
            assertTrue( "Diff. Min->setMax->setMin :" + item + " - " + cmpStr, cmpStr == null );
        }

        // All binary digit should be modify !
        String cmpXORAndMaxStr = MiiTestHelper.getMaxMiiFilledWith0xFF().advanceCompareTo( xorBytes );
        assertTrue( "All binary digit should be modify : " + cmpXORAndMaxStr, cmpXORAndMaxStr == null );

        slogger.info( "--");
    }

    /**
     * Verify that all bits can be modified.
     * Set max values on all setter, compare to maxMii, set to min values, and compare to emptyMii (minMii).
     * @throws IntrospectionInvokeException
     * @throws MiiGetterIntrospectionBadParameterException
     */
    public void testGetterSetterMaxMin() throws IntrospectionInvokeException, MiiGetterIntrospectionBadParameterException
    {
        slogger.info( "---------- testGetterSetterEffects()");
        Map<String,MiiGetterIntrospectionItem> items = miiGetterIntrospection.getMap();
        Mii tmpMii = new Mii( minMii );

        for( MiiGetterIntrospectionItem item : items.values() ) {
            //MIIGETTER      anno = item.getAnnotation();
            MIIGETTER.Type type = item.getType();

            if( type == MIIGETTER.Type.BOOL ) {
                item.setBooleanValue( tmpMii, MAX_BOOL );
            } else if( type == MIIGETTER.Type.BYTES4 ) {
                item.setObjectValue( tmpMii, MAX_BYTES4 );
            } else if( type == MIIGETTER.Type.INT ) {
                item.setIntegerValue( tmpMii, MAX_INT );
            } else if( type == MIIGETTER.Type.RAWSTRING ) {
                item.setStringValue( tmpMii, MAX_RAWSTRING );
            } else {
                fail( "Unkwon type  MIIGETTER.Type = " + type );
            }
        }

        String cmpMaxStr = MiiTestHelper.getMaxMiiFilledWith0xFF().advanceCompareTo( tmpMii );
        assertTrue( "Diff. cmpMaxStrn :" + cmpMaxStr, cmpMaxStr == null );

        for( MiiGetterIntrospectionItem item : items.values() ) {
            //MIIGETTER      anno = item.getAnnotation();
            MIIGETTER.Type type = item.getType();

            if( type == MIIGETTER.Type.BOOL ) {
                item.setBooleanValue( tmpMii, MIN_BOOL );
            } else if( type == MIIGETTER.Type.BYTES4 ) {
                item.setObjectValue( tmpMii, MIN_BYTES4 );
            } else if( type == MIIGETTER.Type.INT ) {
                item.setIntegerValue( tmpMii, MIN_INT );
            } else if( type == MIIGETTER.Type.RAWSTRING ) {
                item.setStringValue( tmpMii, MIN_RAWSTRING );
            } else {
                fail( "Unkwon type  MIIGETTER.Type = " + type );
            }
        }

        String cmpMinStr = minMii.advanceCompareTo( tmpMii );
        assertTrue( "Diff. cmpMinStr :" + cmpMinStr, cmpMinStr == null );

        slogger.info( "--");
    }

    /**
     * TODO: set values on all values using setters, and verify byte[] mii is all 0xFF ! something like that
     */
    public void testSetterRange()
    {
        // Mii aMii = new Mii();


    }

    /**
     * Clone Mii using getters and setters, and compare both Mii Objects using
     * binary comparison.
     * @throws IntrospectionException
     */
    public void testCloneUsingGettersAndSettersAndBinCompare() throws IntrospectionException
    {
        slogger.info( "---------- Clone mii using getters/setters --------------");
        Map<String,MiiGetterIntrospectionItem> items = miiGetterIntrospection.getMap();
        Mii cpy = new Mii();

        for( Map.Entry<String, MiiGetterIntrospectionItem> entry : items.entrySet() ) {
            //final String                   key  = entry.getKey();
            final MiiGetterIntrospectionItem item = entry.getValue();
            final Method getter = item.getGetterMethod();
            final Method setter = item.getSetterMethod();

            if( getter == null ) {
                slogger.error( "** Getter for " + item + " is not define !" );
            }
            else if( setter == null ) {
                slogger.error( "** Setter for " + item + " is not define !" );
            }
            else {
                //MIIGETTER       anno = item.getAnnotation();
                MIIGETTER.Type  type = item.getType();

                if( type == MIIGETTER.Type.BOOL ) {
                    item.setBooleanValue( cpy, item.getBooleanValue( avatarFileMii ) );

                    slogger.info( entry.getKey() + " = " + item.getBooleanValue( cpy ) );
                }
                else if( type == MIIGETTER.Type.BYTES4 ) {
                    item.setObjectValue( cpy,item.getBytesValue( avatarFileMii ) );

                    slogger.info( entry.getKey() + " = " + BinStuffs.toHexString( item.getBytesValue( cpy ) ) );
                }
                else if( type == MIIGETTER.Type.INT ) {
                    item.setIntegerValue( cpy, item.getIntegerValue( avatarFileMii ) );

                    slogger.info( entry.getKey() + " = " + item.getIntegerValue( cpy ) );
                }
                else if( type == MIIGETTER.Type.RAWSTRING ) {
                    item.setStringValue( cpy, item.getStringValue( avatarFileMii ) );

                    slogger.info( entry.getKey() + " = " + item.getStringValue( cpy ) );
                }
                else {
                    slogger.error( "** Getter/Setter not handle type: " +  item + " (" + type + ")" );
                }
            }
        }

        slogger.info( "---------- Binary compare original and clone ------------");
        String cmpStr = avatarFileMii.advanceCompareTo( cpy );
        int    cmp    = avatarFileMii.compareTo( cpy );

        slogger.info( "---------- Binary compare: Check cmp methods" );
        // Verify both compare give same result
        assertTrue( "compare compare :) ", (cmp == 0) == (cmpStr == null) );

        slogger.debug( "cmpStr = " + cmpStr );

        slogger.info( "---------- Binary compare: Check junit method" );
        assertTrue( "Binary compare with copy (junit)=" + cmpStr, cmpStr == null );

        slogger.info( "---------- Binary compare: Check std method" );
        assertTrue( "Binary compare with copy", cmp == 0 );

        slogger.info( "---------------------------------------------------------");
    }

    /**
     * Clone Mii using clone method, and compare both Mii Objects using getters
     * and setters.
     * @throws IntrospectionException
     */
    public void testCloneUsingGettersAndSetters() throws IntrospectionException
    {
        slogger.info( "---------- Clone mii using getters/setters --------------");
        //Map<String,MiiGetterInspection.Item> items = miiGetterInspection.getItemMap();
        Mii miiCopy = new Mii( avatarFileMii );

        slogger.info( "---------- Binary compare original and clone ------------");
        int cmpBin = avatarFileMii.compareTo( miiCopy );

        assertTrue( "Binary compare with copy", cmpBin == 0 );

        slogger.info( "---------- Compare original and clone -------------------");
        int cmp = runCompareWithException( avatarFileMii, miiCopy );

        assertTrue( "Compare with copy", cmp == 0 );

        slogger.info( "---------------------------------------------------------");
    }



    /**
     * Verify that getter and setter are consistent according to type and annotations
     * @throws IOException
     */
    public void testSave() throws IOException
    {
        slogger.info( "---------- testSave() ------------------------------------");
        Mii aMii = MiiTestHelper.getRandomMii();

        File tmpFileDelete = File.createTempFile( this.getClass().getName(), "#testSave.delete.after.save" );

        aMii.save( tmpFileDelete );

        boolean del0 = tmpFileDelete.delete();

        // verify lock
        assertTrue( "Can not delete file after saving.", del0 );

        File tmpFile = File.createTempFile( this.getClass().getName(), "#testSave" );

        aMii.save( tmpFile );

        Mii newMii = new Mii( tmpFile );
        boolean del1 = tmpFile.delete();

        // verify lock
        assertTrue( "Can not delete file after reading.", del1 );

        slogger.info( "---------- Compare original and clone -------------------");
        int cmp = runCompareWithException( aMii, newMii );

        assertTrue( "Compare with copy", cmp == 0 );

        slogger.info( "---------------------------------------------------------");
    }

    // ----------------------------------------------------------------------------
    // -------- TOOLS -------------------------------------------------------------
    // ----------------------------------------------------------------------------

    private int runCompareWithException( Mii mii1, Mii mii2 )
    {
        int res;

        try {
            res = miiGetterIntrospection.compareWithException( mii1, mii2 );

            assertTrue( "Should be alway true here", res == 0 );
        }
        catch( IntrospectionCompareException e ) {
            slogger.info( "Compare diff found using: " + e.getMethod() + " - " + e.getMessage() );

            res = e.getCompareValue();
        }
        catch( IntrospectionInvokeException e ) {
            slogger.warn( "Exception while compare: " + e.getMethod(), e );

            res = Integer.MIN_VALUE;
        }

        return res;
    }

//    public static final Mii getMinMii()
//    {
//        return new AbtractTestMiiBuilder() {
//
//            @Override
//            public byte getByte()
//            {
//                return 0;
//            }
//
//        }.getMii();
//    }
//
//    public static final Mii getMaxMii()
//    {
//        return new AbtractTestMiiBuilder() {
//
//            @Override
//            public byte getByte()
//            {
//                return (byte)0xFF;
//            }
//
//        }.getMii();
//    }
//
//    public static final Mii getRandomMii()
//    {
//        final Random ran = new Random();
//        final byte[] b   = new byte[ 1 ];
//
//        return new AbtractTestMiiBuilder() {
//
//            @Override
//            public byte getByte()
//            {
//                ran.nextBytes( b );
//                return b[ 0 ];
//            }
//
//        }.getMii();
//    }
//
//    abstract static class AbtractTestMiiBuilder
//    {
//        public abstract byte getByte();
//
//        public Mii getMii()
//        {
//            try {
//                return new Mii( new InputStream() {
//
//                    @Override
//                    public int read() throws IOException
//                    {
//                        return 0x00FF & getByte();
//                    }
//
//                } );
//            }
//            catch( IOException e ) {
//                throw new RuntimeException( e );
//            }
//        }
//    }
}